/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {assertVariableValues} from './test_helpers/variables.js';
import {
  createGenUidStubWithReturns,
  sharedTestSetup,
  sharedTestTeardown,
} from './test_helpers/setup_teardown.js';
import {
  assertEventFired,
  assertEventNotFired,
  createChangeListenerSpy,
} from './test_helpers/events.js';

suite('Variable Map', function () {
  setup(function () {
    sharedTestSetup.call(this);
    this.workspace = new Blockly.Workspace();
    this.variableMap = new Blockly.VariableMap(this.workspace);
  });

  teardown(function () {
    sharedTestTeardown.call(this);
  });

  suite('createVariable', function () {
    test('Trivial', function () {
      this.variableMap.createVariable('name1', 'type1', 'id1');
      assertVariableValues(this.variableMap, 'name1', 'type1', 'id1');
    });

    test('Already exists', function () {
      // Expect that when the variable already exists, the variableMap is unchanged.
      this.variableMap.createVariable('name1', 'type1', 'id1');

      // Assert there is only one variable in the this.variableMap.
      let keys = Array.from(this.variableMap.variableMap.keys());
      chai.assert.equal(keys.length, 1);
      let varMapLength = this.variableMap.variableMap.get(keys[0]).length;
      chai.assert.equal(varMapLength, 1);

      this.variableMap.createVariable('name1', 'type1');
      assertVariableValues(this.variableMap, 'name1', 'type1', 'id1');
      // Check that the size of the variableMap did not change.
      keys = Array.from(this.variableMap.variableMap.keys());
      chai.assert.equal(keys.length, 1);
      varMapLength = this.variableMap.variableMap.get(keys[0]).length;
      chai.assert.equal(varMapLength, 1);
    });

    test('Name already exists', function () {
      // Expect that when a variable with the same name but a different type already
      // exists, the new variable is created.
      this.variableMap.createVariable('name1', 'type1', 'id1');

      // Assert there is only one variable in the this.variableMap.
      let keys = Array.from(this.variableMap.variableMap.keys());
      chai.assert.equal(keys.length, 1);
      const varMapLength = this.variableMap.variableMap.get(keys[0]).length;
      chai.assert.equal(varMapLength, 1);

      this.variableMap.createVariable('name1', 'type2', 'id2');
      assertVariableValues(this.variableMap, 'name1', 'type1', 'id1');
      assertVariableValues(this.variableMap, 'name1', 'type2', 'id2');
      // Check that the size of the variableMap did change.
      keys = Array.from(this.variableMap.variableMap.keys());
      chai.assert.equal(keys.length, 2);
    });

    test('Null type', function () {
      this.variableMap.createVariable('name1', null, 'id1');
      assertVariableValues(this.variableMap, 'name1', '', 'id1');
    });

    test('Undefined type', function () {
      this.variableMap.createVariable('name2', undefined, 'id2');
      assertVariableValues(this.variableMap, 'name2', '', 'id2');
    });

    test('Null id', function () {
      createGenUidStubWithReturns('1');
      this.variableMap.createVariable('name1', 'type1', null);
      assertVariableValues(this.variableMap, 'name1', 'type1', '1');
    });

    test('Undefined id', function () {
      createGenUidStubWithReturns('1');
      this.variableMap.createVariable('name1', 'type1', undefined);
      assertVariableValues(this.variableMap, 'name1', 'type1', '1');
    });

    test('Two variables same type', function () {
      this.variableMap.createVariable('name1', 'type1', 'id1');
      this.variableMap.createVariable('name2', 'type1', 'id2');

      assertVariableValues(this.variableMap, 'name1', 'type1', 'id1');
      assertVariableValues(this.variableMap, 'name2', 'type1', 'id2');
    });

    test('Two variables same name', function () {
      this.variableMap.createVariable('name1', 'type1', 'id1');
      this.variableMap.createVariable('name1', 'type2', 'id2');

      assertVariableValues(this.variableMap, 'name1', 'type1', 'id1');
      assertVariableValues(this.variableMap, 'name1', 'type2', 'id2');
    });

    suite('Error cases', function () {
      test('Id already exists', function () {
        this.variableMap.createVariable('name1', 'type1', 'id1');
        const variableMap = this.variableMap;
        chai.assert.throws(function () {
          variableMap.createVariable('name2', 'type2', 'id1');
        }, /"id1".*in use/);
        assertVariableValues(this.variableMap, 'name1', 'type1', 'id1');
      });

      test('Mismatched id', function () {
        this.variableMap.createVariable('name1', 'type1', 'id1');
        const variableMap = this.variableMap;
        chai.assert.throws(function () {
          variableMap.createVariable('name1', 'type1', 'id2');
        }, /"name1".*in use/);
        assertVariableValues(this.variableMap, 'name1', 'type1', 'id1');
      });

      test('Mismatched type', function () {
        this.variableMap.createVariable('name1', 'type1', 'id1');
        const variableMap = this.variableMap;
        chai.assert.throws(function () {
          variableMap.createVariable('name1', 'type2', 'id1');
        });
        assertVariableValues(this.variableMap, 'name1', 'type1', 'id1');
        chai.assert.isNull(this.variableMap.getVariableById('id2'));
      });
    });
  });

  suite('getVariable', function () {
    test('By name and type', function () {
      const var1 = this.variableMap.createVariable('name1', 'type1', 'id1');
      const var2 = this.variableMap.createVariable('name2', 'type1', 'id2');
      const var3 = this.variableMap.createVariable('name3', 'type2', 'id3');
      const result1 = this.variableMap.getVariable('name1', 'type1');
      const result2 = this.variableMap.getVariable('name2', 'type1');
      const result3 = this.variableMap.getVariable('name3', 'type2');

      // Searching by name + type is correct.
      chai.assert.equal(result1, var1);
      chai.assert.equal(result2, var2);
      chai.assert.equal(result3, var3);

      // Searching only by name defaults to the '' type.
      chai.assert.isNull(this.variableMap.getVariable('name1'));
      chai.assert.isNull(this.variableMap.getVariable('name2'));
      chai.assert.isNull(this.variableMap.getVariable('name3'));
    });

    test('Not found', function () {
      const result = this.variableMap.getVariable('name1');
      chai.assert.isNull(result);
    });
  });

  suite('getVariableById', function () {
    test('Trivial', function () {
      const var1 = this.variableMap.createVariable('name1', 'type1', 'id1');
      const var2 = this.variableMap.createVariable('name2', 'type1', 'id2');
      const var3 = this.variableMap.createVariable('name3', 'type2', 'id3');
      const result1 = this.variableMap.getVariableById('id1');
      const result2 = this.variableMap.getVariableById('id2');
      const result3 = this.variableMap.getVariableById('id3');

      chai.assert.equal(result1, var1);
      chai.assert.equal(result2, var2);
      chai.assert.equal(result3, var3);
    });

    test('Not found', function () {
      const result = this.variableMap.getVariableById('id1');
      chai.assert.isNull(result);
    });
  });

  suite('getVariableTypes', function () {
    test('Trivial', function () {
      this.variableMap.createVariable('name1', 'type1', 'id1');
      this.variableMap.createVariable('name2', 'type1', 'id2');
      this.variableMap.createVariable('name3', 'type2', 'id3');
      this.variableMap.createVariable('name4', 'type3', 'id4');
      const resultArray = this.variableMap.getVariableTypes();
      // The empty string is always an option.
      chai.assert.deepEqual(resultArray, ['type1', 'type2', 'type3', '']);
    });

    test('None', function () {
      // The empty string is always an option.
      const resultArray = this.variableMap.getVariableTypes();
      chai.assert.deepEqual(resultArray, ['']);
    });
  });

  suite('getVariablesOfType', function () {
    test('Trivial', function () {
      const var1 = this.variableMap.createVariable('name1', 'type1', 'id1');
      const var2 = this.variableMap.createVariable('name2', 'type1', 'id2');
      this.variableMap.createVariable('name3', 'type2', 'id3');
      this.variableMap.createVariable('name4', 'type3', 'id4');
      const resultArray1 = this.variableMap.getVariablesOfType('type1');
      const resultArray2 = this.variableMap.getVariablesOfType('type5');
      chai.assert.deepEqual(resultArray1, [var1, var2]);
      chai.assert.deepEqual(resultArray2, []);
    });

    test('Null', function () {
      const var1 = this.variableMap.createVariable('name1', '', 'id1');
      const var2 = this.variableMap.createVariable('name2', '', 'id2');
      const var3 = this.variableMap.createVariable('name3', '', 'id3');
      this.variableMap.createVariable('name4', 'type1', 'id4');
      const resultArray = this.variableMap.getVariablesOfType(null);
      chai.assert.deepEqual(resultArray, [var1, var2, var3]);
    });

    test('Empty string', function () {
      const var1 = this.variableMap.createVariable('name1', null, 'id1');
      const var2 = this.variableMap.createVariable('name2', null, 'id2');
      const resultArray = this.variableMap.getVariablesOfType('');
      chai.assert.deepEqual(resultArray, [var1, var2]);
    });

    test('Deleted', function () {
      const variable = this.variableMap.createVariable('name1', null, 'id1');
      this.variableMap.deleteVariable(variable);
      const resultArray = this.variableMap.getVariablesOfType('');
      chai.assert.deepEqual(resultArray, []);
    });

    test('Does not exist', function () {
      const resultArray = this.variableMap.getVariablesOfType('type1');
      chai.assert.deepEqual(resultArray, []);
    });
  });

  suite('getAllVariables', function () {
    test('Trivial', function () {
      const var1 = this.variableMap.createVariable('name1', 'type1', 'id1');
      const var2 = this.variableMap.createVariable('name2', 'type1', 'id2');
      const var3 = this.variableMap.createVariable('name3', 'type2', 'id3');
      const resultArray = this.variableMap.getAllVariables();
      chai.assert.deepEqual(resultArray, [var1, var2, var3]);
    });

    test('None', function () {
      const resultArray = this.variableMap.getAllVariables();
      chai.assert.deepEqual(resultArray, []);
    });
  });

  suite('event firing', function () {
    setup(function () {
      this.eventSpy = createChangeListenerSpy(this.workspace);
    });

    teardown(function () {
      this.workspace.removeChangeListener(this.eventSpy);
    });

    suite('variable create events', function () {
      test('create events are fired when a variable is created', function () {
        this.variableMap.createVariable('test name', 'test type', 'test id');

        assertEventFired(
          this.eventSpy,
          Blockly.Events.VarCreate,
          {
            varType: 'test type',
            varName: 'test name',
            varId: 'test id',
          },
          this.workspace.id,
        );
      });

      test('create events are not fired if a variable is already exists', function () {
        this.variableMap.createVariable('test name', 'test type', 'test id');

        this.eventSpy.resetHistory();
        this.variableMap.createVariable('test name', 'test type', 'test id');

        assertEventNotFired(
          this.eventSpy,
          Blockly.Events.VarCreate,
          {},
          this.workspace.id,
        );
      });
    });

    suite('variable delete events', function () {
      suite('deleting with a variable', function () {
        test('delete events are fired when a variable is deleted', function () {
          const variable = this.variableMap.createVariable(
            'test name',
            'test type',
            'test id',
          );
          this.variableMap.deleteVariable(variable);

          assertEventFired(
            this.eventSpy,
            Blockly.Events.VarDelete,
            {
              varType: 'test type',
              varName: 'test name',
              varId: 'test id',
            },
            this.workspace.id,
          );
        });

        test('delete events are not fired when a variable does not exist', function () {
          const variable = new Blockly.VariableModel(
            this.workspace,
            'test name',
            'test type',
            'test id',
          );
          this.variableMap.deleteVariable(variable);

          assertEventNotFired(
            this.eventSpy,
            Blockly.Events.VarDelete,
            {},
            this.workspace.id,
          );
        });
      });

      suite('deleting by ID', function () {
        test('delete events are fired when a variable is deleted', function () {
          this.variableMap.createVariable('test name', 'test type', 'test id');
          this.variableMap.deleteVariableById('test id');

          assertEventFired(
            this.eventSpy,
            Blockly.Events.VarDelete,
            {
              varType: 'test type',
              varName: 'test name',
              varId: 'test id',
            },
            this.workspace.id,
          );
        });

        test('delete events are not fired when a variable does not exist', function () {
          this.variableMap.deleteVariableById('test id');

          assertEventNotFired(
            this.eventSpy,
            Blockly.Events.VarDelete,
            {},
            this.workspace.id,
          );
        });
      });
    });

    suite('variable rename events', function () {
      suite('renaming with variable', function () {
        test('rename events are fired when a variable is renamed', function () {
          const variable = this.variableMap.createVariable(
            'test name',
            'test type',
            'test id',
          );
          this.variableMap.renameVariable(variable, 'new test name');

          assertEventFired(
            this.eventSpy,
            Blockly.Events.VarRename,
            {
              oldName: 'test name',
              newName: 'new test name',
              varId: 'test id',
            },
            this.workspace.id,
          );
        });

        test('rename events are not fired if the variable name already matches', function () {
          const variable = this.variableMap.createVariable(
            'test name',
            'test type',
            'test id',
          );
          this.variableMap.renameVariable(variable, 'test name');

          assertEventNotFired(
            this.eventSpy,
            Blockly.Events.VarRename,
            {},
            this.workspace.id,
          );
        });

        test('rename events are not fired if the variable does not exist', function () {
          const variable = new Blockly.VariableModel(
            'test name',
            'test type',
            'test id',
          );
          this.variableMap.renameVariable(variable, 'test name');

          assertEventNotFired(
            this.eventSpy,
            Blockly.Events.VarRename,
            {},
            this.workspace.id,
          );
        });
      });

      suite('renaming by ID', function () {
        test('rename events are fired when a variable is renamed', function () {
          this.variableMap.createVariable('test name', 'test type', 'test id');
          this.variableMap.renameVariableById('test id', 'new test name');

          assertEventFired(
            this.eventSpy,
            Blockly.Events.VarRename,
            {
              oldName: 'test name',
              newName: 'new test name',
              varId: 'test id',
            },
            this.workspace.id,
          );
        });

        test('rename events are not fired if the variable name already matches', function () {
          this.variableMap.createVariable('test name', 'test type', 'test id');
          this.variableMap.renameVariableById('test id', 'test name');

          assertEventNotFired(
            this.eventSpy,
            Blockly.Events.VarRename,
            {},
            this.workspace.id,
          );
        });

        test('renaming throws if the variable does not exist', function () {
          // Not sure why this throws when the other one doesn't but might
          // as well test it.
          chai.assert.throws(() => {
            this.variableMap.renameVariableById('test id', 'test name');
          }, `Tried to rename a variable that didn't exist`);
        });
      });
    });
  });
});
